.. _глава_32_start:

Функции
=======

.. index::
    function
    функция


Подобно тому, как фундаментальные типы являются строительными блоками данных,
функции являются строительными блоками поведения программы.
Функции тесно связаны с техническим аспектом программирования.
Функции, написанные опытными программистами, лаконичны, просты и
понятны. Это относится к обоим направлениям: Простая попытка идентифицировать и написать меньшие
строительные блоки программы делает программиста лучше.
Мы рассмотрели основные утверждения и выражения в предыдущих главах.
До сих пор использовались стандартные функции языка D. Тем не менее, их самих по себе недостаточно
для написания больших программ. Все программы, которые мы написали до сих пор,
были очень короткими, каждая демонстрировала лишь какую-нибудь простую особенность языка.
Попытка написать программу любого уровня сложности без функций была бы очень сложной и подверженной ошибкам.
В этой главе рассматриваются только основные возможности функций. Мы подробнее поговорим о
функциях позже в следующих главах:

* Параметры функций (:ref:`Параметры функций<глава_35_start>`)
* Перегрузка функций (:ref:`Перегрузка функций (Overloading)<глава_49_start>`)
* Указатели на функции, Делегаты, Лямбда функции (:ref:`Указатели на функции, делегаты и лямбда-выражения<глава_72_start>`)
* Дополнительная информации о функциях (:ref:`Дополнительно о функциях<Дополнительно о функциях>`)


Функции - это блоки кода, которые объединяют операторы и выражения в качестве единиц выполнения
программы. Таким блокам кода в целом присваивается имя, описывающее,
чего они в совокупности достигают. Затем они могут быть вызваны (или выполнены)
с использованием этого имени.
Концепция присвоения названий группе действий распространена в нашей повседневной жизни.
Например, процесс приготовления омлета может быть описан с некоторой степенью детализации
следующими шагами:

* возьмите сковороду
* возьмите масло
* достаньте яйцо
* включите плиту
* поставьте сковороду на огонь
* положите на сковороду сливочное масло, когда оно нагреется
* положите яйцо в масло, когда оно растает
* снимите сковороду с огня, когда яйцо будет готово
* выключите плиту

Поскольку такое количество деталей, очевидно, излишне, связанные между собой шаги
можно объединить под одним названием:
* подготовьте (возьмите сковороду, масло и яйцо)
* включите плиту
* приготовьте яйцо (поставьте сковороду на огонь и т.д.)
* выключите плиту

Далее, можно сказать, что для всех шагов может быть одно название:

* приготовьте омлет из одного яйца (все этапы)

Функции основаны на одной и той же концепции: шаги, которые в совокупности могут быть названы как
единое целое, объединяются в функцию. В качестве примера давайте начнем со
следующих строк кода, которые решают задачу печати меню:

.. code-block:: d

    writeln(" 0 Exit");
    writeln(" 1 Add");
    writeln(" 2 Subtract");
    writeln(" 3 Multiply");
    writeln(" 4 Divide");

Поскольку разумно назвать эти объединенные строки как "печать меню", их можно
объединить в функцию, используя следующий синтаксис:

.. code-block:: d

    void printMenu()
    {
        writeln(" 0 Exit");
        writeln(" 1 Add");
        writeln(" 2 Subtract");
        writeln(" 3 Multiply");
        writeln(" 4 Divide");
    }

Содержимое этой функции теперь может быть вызвано из функции main(), просто
используя по имени:

.. code-block:: d

    void main()
    {
        printMenu();
        // ...
    }

Из сходства определений print Menu() и main() очевидно, что main() также является функцией.
Выполнение программы, написанной на языке D начинается с функции с именем main() и оттуда переходит к другим функциям.

.. index::
    parameter
    параметр

Параметры
---------

Удобство использования функций обусловлено тем, что их поведение настраивается с помощью параметров.
Давайте продолжим пример с омлетом, изменив его, чтобы можно было приготовить омлет из
пяти яиц вместо всегда одного. Шаги будут точно такими же,
разница лишь в количестве используемых яиц. Мы можем соответствующим образом изменить приведенное выше более общее
описание:

* подготовьтесь (возьмите сковороду, масло и пять яиц)
* включить плиту
* приготовите яйца (поставьте сковороду на огонь и т. д.)
* выключите плиту

Аналогичным образом, наиболее общим шагом может стать следующий:

* приготовьте омлет из пяти яиц (все шаги)

На этот раз есть дополнительная информация, которая касается некоторых шагов: "взять
пять яиц", "приготовить яйца" и "сделать омлет из пяти яиц".
Поведение функций может быть скорректировано аналогично примеру с омлетом.
Информация, которую функции используют для настройки своего поведения, называется параметрами.
Параметры указываются в списке параметров функции через запятую.
Список параметров находится внутри круглых скобок, которые следуют после имени функции функции.
Функция printMenu() выше была определена с пустым списком параметров
потому что эта функция всегда печатала одно и то же меню. Предположим, что
иногда в разных контекстах меню нужно будет выводить по-разному. Например,
например, может быть более разумным напечатать первый пункт как "Возврат", а не как
"Выход" в зависимости от того, какая часть программы выполняется в данный момент.
В этом случае первый пункт меню можно параметризовать, задав его в списке параметров
определенным в списке параметров. Тогда функция использует значение этого параметра
вместо литерала "Exit":

.. code-block:: d

    void printMenu(string firstEntry)
    {
        writeln(" 0 ", firstEntry);
        writeln(" 1 Add");
        writeln(" 2 Subtract");
        writeln(" 3 Multiply");
        writeln(" 4 Divide");
    }

Обратите внимание, что поскольку информация, передаваемая параметром firstEntry, представляет собой
текст, его тип в списке параметров указан как string. Эта
функция может быть вызвана с разными значениями параметров для печати меню с разными
разные первые записи. Все, что нужно сделать, - это использовать соответствующие строковые
в зависимости от того, откуда вызывается функция:

.. code-block:: d

    // At some place in the program:
    printMenu("Exit");
    // ...
    // At some other place in the program:
    printMenu("Return");

*Примечание: При написании и использовании собственных функций с параметрами типа
string вы можете столкнуться с ошибками компиляции. Как написано выше, функция printMenu()
не может быть вызвана со значениями параметров типа char[]. Например.
следующий код приведет к ошибке компиляции:*

.. code-block:: d

    char[] anEntry;
    anEntry ~= "Take square root";
    printMenu(anEntry); // ← ошибка компиляции


С другой стороны, если бы функция printMenu() была определена так, чтобы ее параметр принимал значение char[],
то ее нельзя было бы вызывать со строками типа "Exit". Это связано с понятием
неизменяемости и ключевым словом immutable, о которых мы поговорим в
следующей главе.
Продолжим работу с функцией меню и предположим, что не следует
всегда начинать номера выбора меню с нуля. В этом случае начальное
можно передать в функцию в качестве второго параметра. Параметры
параметры функции должны быть разделены запятыми:

.. code-block:: d

    void printMenu(string firstEntry, int firstNumber)
    {
        writeln(' ', firstNumber + 0, ' ', firstEntry);
        writeln(' ', firstNumber + 1, " Add");
        writeln(' ', firstNumber + 2, " Subtract");
        writeln(' ', firstNumber + 3, " Multiply");
        writeln(' ', firstNumber + 4, " Divide");
    }

Теперь можно указать функции, с какого числа начинать:

.. code-block:: d

    printMenu("Return", 1);

Вызов функции
--------------

.. index::
    вызов функции
    call function

Запуск функции, чтобы она выполнила свою задачу, называется вызовом функции. Синтаксис
Синтаксис вызова функции следующий:

.. code-block:: d

    function_name(parameter_values)

Фактические значения параметров, которые передаются функции, называются аргументами функции
аргументами. Хотя термины "параметр" и "аргумент" иногда используются
в литературе как взаимозаменяемые, они обозначают разные понятия.
Аргументы сопоставляются с параметрами один за другим в том порядке, в котором они
параметры определены. Например, в последнем вызове printMenu() выше используются
аргументы "Return" и 1, которые соответствуют параметрам firstEntry и
firstNumber, соответственно.
Тип каждого аргумента должен совпадать с типом соответствующего
параметра.


Выполнение работы
------------------

В предыдущих главах мы определяли выражения как сущности, которые выполняют работу.
Вызовы функций тоже являются выражениями: они выполняют определенную работу. Выполнение работы означает
производить значение или создавать побочный эффект:

* Производить значение: Некоторые операции производят только значения. Например.
  функция, которая складывает числа, производит результат сложения. Как
  другой пример, функция, которая создает объект Student, используя имя и адрес студента.
  имя и адрес студента, будет производить объект Student.
* Наличие побочных эффектов: Побочные эффекты - это любые изменения в состоянии программы или
  ее окружения. Некоторые операции имеют только побочные эффекты. Примером может служить то, как
  приведенная выше функция printMenu() изменяет stdout, печатая в него. В качестве другого
  пример, функция, которая добавляет объект Student в контейнер student.
  также имеет побочный эффект: она заставляет контейнер увеличиваться.
  В общем, операции, которые приводят к изменению состояния программы
  создают побочные эффекты.
* Имеют побочные эффекты и производят значение: Некоторые операции делают и то, и другое. Например,
  например, функция, которая считывает два значения из stdin и возвращает их сумму
  будет иметь побочные эффекты из-за изменения состояния stdin, а также
  производит сумму двух значений.
* Нет операции: Хотя каждая функция проектируется как одна из трех
  категорий, в зависимости от определенных условий во время компиляции или во время выполнения,
  некоторые функции не выполняют никаких операций.
  В зависимости от определенных условий во время компиляции или выполнения некоторые функции вообще не выполняют никакой работы.

Возвращаемое значение
---------------------

.. index::
    возвращаемое значение
    return value

Значение, которое функция выдает в результате своей работы, называется ее возвращаемым значением.
Этот термин поскольку, после того, как выполнение программы разветвляется
в функцию, она в конечном итоге возвращается туда, откуда была вызвана.
Мы "вызываем" функцию и используем значение, которое она "возвращает".
Как и у любого другого значения, у возвращаемых значений есть тип. Тип возвращаемого значения
указывается непосредственно перед именем функции, в том месте, где функция
определяется. Например, функция, которая складывает два значения типа int и возвращает
их сумму также в виде int, будет определена следующим образом:

.. code-block:: d

    int add(int first, int second)
    {
    // ... the actual work of the function ...
    }

Значение, которое возвращает функция, занимает место самого вызова функции. Для
Например, если предположить, что вызов функции add(5, 7) выдает значение 12, то
следующие две строки будут эквивалентны:

.. code-block:: d

    writeln("Result: ", add(5, 7));
    writeln("Result: ", 12);

В первой строке выше функция add() вызывается с аргументами 5 и 7
перед вызовом функции writeln(). Значение 12, которое возвращает функция, в свою очередь
передается в writeln() в качестве второго аргумента.
Это позволяет передавать возвращаемые значения функций другим функциям для формирования
сложные выражения:

.. code-block:: d

    writeln("Result: ", add(5, divide(100, studentCount())));

В строке выше возвращаемое значение studentCount() передается в divide() в качестве второго аргумента.
в качестве второго аргумента, возвращаемое значение divide() передается в add() в качестве второго аргумента, и в конечном итоге возвращаемое значение add() передается в
второй аргумент, и в конечном итоге возвращаемое значение add() передается в
writeln() в качестве второго аргумента.

.. index::
    retrurn


Оператор return
----------------

Возвращаемое значение функции задается ключевым словом return:

.. code-block:: d

    int add(int first, int second)
    {
        int result = first + second;
        return result;
    }

Функция создает свое возвращаемое значение, используя операторы,
выражений и, возможно, путем вызова других функций. Затем функция
возвращает это значение с помощью ключевого слова return, после чего выполнение функции
функции заканчивается.
В функции может быть более одного оператора return. Значение
первого оператора возврата, который будет выполнен, определяет возвращаемое значение
функции для конкретного вызова:

.. code-block:: d

    int complexCalculation(int aParameter, int anotherParameter)
    {
        if (aParameter == anotherParameter)
        {
            return 0;
        }
        return aParameter * anotherParameter;
    }

.. index::
    void

Функции void
------------

Типы возвращаемых знаяений функций, которые не производят значений, указываются как void. Мы
уже много раз видели это в функции main(), а также в функции
printMenu(). Поскольку они не возвращают никаких значений вызывающей стороне,
их типы возврата были определены как void.
*(Примечание: функция main() также может быть определена как
возвращающую int. Мы рассмотрим это в одной из следующих глав* (:ref:`Программное окружение<Программное окружение>` )).

Функции void
Типы возврата функций, которые не производят значений, указываются как void. Мы
уже много раз видели это в функции main(), а также в функции
printMenu(). Поскольку они не возвращают никаких значений вызывающей стороне,
их типы возврата были определены как void. (Примечание: функция main() также может быть определена как
возвращающую int. Мы рассмотрим это в одной из следующих глав (стр. 185)).

.. index::
    имя функции


Имя функции
-----------

Имя функции должно быть выбрано таким образом, чтобы четко передавать ее назначение.
Например, имена add и printMenu были подходящими.
потому что они предназначены для добавления двух значений и печати меню, соответственно.
Общим правилом для имен функций является то, что они должны содержать глагол, например add или
print. Согласно этому правилу, такие имена, как add() и menu(), были бы
менее чем идеальными.
Однако вполне допустимо называть функции просто существительными, если эти функции
не имеют побочных эффектов. Например, функцию, которая возвращает текущую
температуру, можно назвать currentTemperature(), а не
getCurrentTemperature().
Придумывание понятных, коротких и согласованных имен - часть тонкого
искусства программирования.

Улучшение качества кода с помощью функций
-----------------------------------------

Функции могут улучшить качество кода. Небольшие функции с меньшим
количеством обязанностей приводят к тому, что программы становятся проще в обслуживании.

.. index::
    дублирование кода

**Дублирование кода вредно**

Одним из аспектов, сильно снижающих качество программы, является дублирование кода
дублирование. Дублирование кода происходит, когда в программе имеется более одного фрагмента кода, выполняющего одну и ту же задачу.
Хотя иногда это происходит путем копирования строк кода, это также может
это может происходить и случайно при написании отдельных частей кода.
Одна из проблем с частями кода, которые дублируют по сути одну и ту же
функциональность, заключается в том, что они создают множество шансов для появления ошибок. Когда
когда такие ошибки возникают и нам нужно их исправить, бывает трудно убедиться, что мы
что мы исправили все места, где возникла проблема, поскольку они могут быть разбросаны
вокруг. И наоборот, если код появляется только в одном месте программы, то
нам нужно исправить его только в одном месте, чтобы избавиться от ошибки раз и навсегда.
Как я уже говорил, функции тесно связаны с ремесленным аспектом
программирования. Опытные программисты всегда следят за тем, чтобы код
дублирования. Они постоянно пытаются выявить общие черты в коде и перенести
общие фрагменты кода в отдельные функции (или в общие структуры, классы,
шаблоны и т. д., как мы увидим в последующих главах).
Давайте начнем с программы, которая содержит некоторое дублирование кода. Посмотрим, как
это дублирование можно устранить, переместив код в функции (т. е. проведя рефакторинг
кода). Следующая программа считывает числа из входных данных и печатает их
сначала в порядке их поступления, а затем в числовом порядке:

.. code-block:: d

    import std.stdio;
    import std.algorithm;

    void main()
    {
        int[] numbers;
        int count;
        write("How many numbers are you going to enter? ");
        readf(" %s", &count);
        // Read the numbers
        foreach (i; 0 .. count)
        {
            int number;
            write("Number ", i, "? ");
            readf(" %s", &number);
            numbers ~= number;
        }
        // Print the numbers
        writeln("Before sorting:");
        foreach (i, number; numbers)
        {
            writefln("%3d:%5d", i, number);
        }
        sort(numbers);
        // Print the numbers
        writeln("After sorting:");
        foreach (i, number; numbers)
        {
            writefln("%3d:%5d", i, number);
        }
    }


Есть чем заняться. Обратите внимание, что строка заголовка всегда выводится непосредственно перед
печати элементов фрагмента. Хотя заголовок разный, задача одна и та же
одна и та же. Если печать заголовка можно рассматривать как часть печати фрагмента, то заголовок тоже
можно передавать в качестве параметра. Вот новые изменения:

.. code-block:: d

    void print(string title, int[] slice)
    {
        writeln(title, ":");
        foreach (i, element; slice)
        {
            writefln("%3s:%5s", i, element);
        }
    }
    // ...
    // Print the numbers
    print("Before sorting", numbers);
    // ...
    // Print the numbers
    print("After sorting", numbers);


Этот шаг позволяет избавиться от комментариев, которые появляются непосредственно
перед двумя вызовами print(). Поскольку имя функции уже ясно
ясно говорит о том, что она делает, эти комментарии излишни:

.. code-block:: d

    print("Before sorting", numbers);
    sort(numbers);
    print("After sorting", numbers);

В этой программе, хотя и незаметно, больше дублирования кода: Значения
count и number считываются абсолютно одинаково. Единственное различие заключается в
сообщение, которое выводится пользователю, и имя переменной:

.. code-block:: d

    int count;
    write("How many numbers are you going to enter? ");
    readf(" %s", &count);
    // ...
    int number;
    write("Number ", i, "? ");
    readf(" %s", &number);

Код станет еще лучше, если он воспользуется новой функцией, которая
которую можно было бы назвать подходящим образом readInt(). Новая функция может принимать
сообщение в качестве параметра, печатать это сообщение, считывать int из входных данных и
возвращать это значение:

.. code-block:: d

    int readInt(string message)
    {
        int result;
        write(message, "? ");
        readf(" %s", &result);
        return result;
    }

count теперь может быть инициализирован непосредственно возвращаемым значением вызова этой новой
функции:

.. code-block:: d

    int count = readInt("How many numbers are you going to enter");

number не может быть инициализировано столь же простым способом, поскольку счетчик цикла
счетчик i является частью сообщения, которое выводится при чтении
число. Эту проблему можно решить, воспользовавшись преимуществами функции формата:

.. code-block:: d

    import std.string;
    // ...
    int number = readInt(format("Number %s", i));

Давайте внесем последние изменения в эту программу, переместив строки, которые считывают
чисел в отдельную функцию. Это также устранит необходимость в комментарии "Read
чисел", поскольку имя новой функции уже будет
будет содержать эту информацию.
Новая функция readNumbers() не нуждается ни в каких параметрах для выполнения своей
для выполнения своей задачи. Она считывает некоторые числа и возвращает их в виде фрагмента. Ниже приведена окончательная
версия программы:

.. code-block:: d

    import std.stdio;
    import std.string;
    import std.algorithm;

    void print(string title, int[] slice)
    {
        writeln(title, ":");
        foreach (i, element; slice)
        {
            writefln("%3s:%5s", i, element);
        }
    }

    int readInt(string message)
    {
        int result;
        write(message, "? ");
        readf(" %s", &result);
        return result;
    }

    int[] readNumbers()
    {
        int[] result;
        int count =
            readInt("How many numbers are you going to enter");
        foreach (i; 0 .. count)
        {
            result ~= readInt(format("Number %s", i));
        }
        return result;
    }

    void main()
    {
        int[] numbers = readNumbers();
        print("Before sorting", numbers);
        sort(numbers);
        print("After sorting", numbers);
    }


Сравните эту версию программы с первой. Основные шаги
очень хорошо видны в функции main() новой программы. Напротив,
функция main() первой программы должна была быть тщательно изучена, чтобы
чтобы понять цель этой программы.
Хотя общее количество нетривиальных строк в двух версиях программы
в этом примере оказалось одинаковым, функции делают программы короче
в целом. В этой простой программе этот эффект не очевиден. Например, до того, как
функция readInt() была определена, чтение int из входных данных занимало
три строки кода. После определения функции readInt() та же цель достигается с помощью
одной строкой кода. Кроме того, определение readInt() позволило отказаться от
определение переменной number.

**Закомментированные строки кода как функции**

Иногда необходимость написать комментарий, чтобы описать назначение группы
строк кода - это признак того, что эти строки лучше перенести в новую
определенную функцию. Если имя функции достаточно описательно, то
комментарий также не нужен.
Три закомментированные группы строк первой версии программы были
были использованы для определения новых функций, выполняющих те же задачи.
Еще одно важное преимущество удаления строк с комментариями заключается в том, что комментарии имеют тенденцию
со временем устаревают, поскольку код модифицируется. Обновляя код,
программисты иногда забывают обновить связанные с ним комментарии, в результате чего эти
Комментарии становятся бесполезными или, что еще хуже, вводят в заблуждение. По этой причине
полезно стараться писать программы без комментариев.

Упражнения
----------

#. Измените функцию printMenu() так, чтобы она принимала в качестве параметра весь набор пунктов меню
параметр. Например, пункты меню можно передать в функцию, как показано в
следующем коде:

.. code-block:: d

    string[] items = [ "Black", "Red", "Green", "Blue", "White" ];
    printMenu(items, 1);

Программа должна выдасть следующий результат:

.. code-block:: console

    1 Black
    2 Red
    3 Green
    4 Blue
    5 White

В следующей программе  используется двумерный массив. Начните с
этой программы и улучшите ее, добавив в нее больше функциональности:

.. code-block:: d

    import std.stdio;

    enum totalLines = 20;
    enum totalColumns = 60;
    /* The 'alias' in the next line makes 'Line' an alias of
        * dchar[totalColumns]. Every 'Line' that is used in the rest
        * of the program will mean dchar[totalColumns] from this
        * point on.
        *
        * Also note that 'Line' is a fixed-length array. */
    *
        alias Line = dchar[totalColumns];
    /* A dynamic array of Lines is being aliased as 'Canvas'. */
    alias Canvas = Line[];
    /* Prints the canvas line by line. */
    void print(Canvas canvas)
    {
        foreach (line; canvas)
        {
            writeln(line);
        }
    }
    /* Places a dot at the specified location on the canvas. In a
        * sense, "paints" the canvas. */
    void putDot(Canvas canvas, int line, int column)
    {
        canvas[line][column] = '#';
    }
    /* Draws a vertical line of the specified length from the
        * specified position. */
    void drawVerticalLine(Canvas canvas,
        int line,
        int column,
        int length)
    {
        foreach (lineToPaint; line .. line + length)
        {
            putDot(canvas, lineToPaint, column);
        }
    }

    void main()
    {
        Line emptyLine = '.';
        /* An empty canvas */
        Canvas canvas;
        /* Constructing the canvas by adding empty lines */
        foreach (i; 0 .. totalLines)
        {
            canvas ~= emptyLine;
        }
        /* Using the canvas */
        putDot(canvas, 7, 30);
        drawVerticalLine(canvas, 5, 10, 4);
        print(canvas);
    }
